home *** CD-ROM | disk | FTP | other *** search
/ PC World 2008 September / PCWorld_2008-09_cd.bin / v cisle / sadanastroju / IE7proSetup_2.3.exe / userscripts / BookBurro.ieuser.js next >
Text File  |  2007-11-20  |  43KB  |  1,110 lines

  1. // Bookburro Ajax Panel for GreaseMonkey and Turnabout
  2. // @version 0.18
  3. /*
  4.  
  5.  (C) 2005 Johan Sundstr├╢m (0.13 .. 0.18)
  6.  License: Creative Commons "Attribution-ShareAlike 2.0"
  7.  http://creativecommons.org/licenses/by-sa/2.0/
  8.  
  9.  Skeletal parts of DOM-Drag by Aaron Boodman, 2001
  10.  http://www.youngpup.net/2001/domdrag
  11.  License: Creative Commons "Attribution-ShareAlike 2.0"
  12.  http://creativecommons.org/licenses/by-sa/2.0/
  13.  
  14.  (C) 2005 Reify (0.11r .. 0.12r)
  15.  License: Creative Commons "Attribution-ShareAlike 1.0"
  16.  http://creativecommons.org/licenses/by-sa/1.0/
  17.  
  18.  (C) 2005 Jesse Andrews, Britt Selvitelle under cc-by-sa (0.01 .. 0.11)
  19.  License: Creative Commons "Attribution-ShareAlike 1.0"
  20.  http://creativecommons.org/licenses/by-sa/1.0/
  21.  
  22.  Snipits used from RSS Reader for GreaseMonkey
  23.  http://www.xs4all.nl/~jlpoutre/BoT/Javascript/RSSpanel/
  24.  Version: 1.03 (C) 2005 Johannes la Poutre
  25.  THANKS!
  26.  
  27.  Changelog:
  28.  
  29.   *  2006-01-09  *
  30.  
  31.  0.18 - Added bookpool.com, contributed by Pallando.
  32.  
  33.   *  2005-11-02  *
  34.  
  35.  0.17 - Added Google Print. Initial preparations for ISBN-13, for Jan. 1, 2007.
  36.  
  37.   *  2005-10-04  *
  38.  
  39.  0.16 - Bugfix for Greasemonkey 0.53 -- for now, only open/close/drag the title
  40.  
  41.   *  2005-10-03  *
  42.  
  43.  0.15 - Added BiggerBooks.com, Bookbyte, Booksamillion.com and eCampus.com
  44.  
  45.   *  2005-10-02  *
  46.  
  47.  0.14 - Made the bookburro window draggable
  48.       - Added (one hour) price cache for last seen book
  49.       - Added Abebooks, Alibris, biblio.com, cdon.com, exlibris.se and libris.se
  50.       - Centralized all (functionally important) code dealing with each store
  51.       - Integrated my improvements with the Reified version 0.12r codebase
  52.       - Added option to update book URL when fetching price tag
  53.       / Johan Sundstr├╢m, oyasumi+bookburro@gmail.com
  54.  
  55.   *  2005-09-19  *
  56.  
  57.  0.13 - GreaseMonkey 0.6 compatibility fix to get working onclick handlers
  58.       - Added AJAX fetching from Powell's Books, figuring they want customers
  59.       - Added adlibris.se, akademibokhandeln.se, bokus.com and internetbokhandeln.se
  60.       - For kicks, added availability listing at Link├╢ping University library
  61.       / Johan Sundstr├╢m, oyasumi+bookburro@gmail.com
  62.  
  63.   *  2005-06-24  *
  64.  
  65.  0.12x - Added support for alldirect.com
  66.  
  67.   *  2005-06-21  *
  68.  
  69.  0.12r - works when injected before the page has finished loading; fixed price-parsing on Powells.com; removed Powells from list in the Burro box
  70.  
  71.  0.11r - modified by Reify to work in Internet Explorer with Turnabout:
  72.      - The price wouldn't render on Amazon for some reason, so we now render with DIVs + SPANs
  73.      - Set style in a way that works in IE (element.setAttribute("style", str) doesn't work in IE)
  74.      - Moved data: URIs to one place to make the script easier to read
  75.      - Show/hide the price table rather than changing the box's dimensions to make code easier to maintain (don't have dimensions scattered throughout)
  76.      - Anchored top right icons to the top right instead of top left so their position will update automatically if the box has to be wider
  77.      - Branch to use MS or Mozilla XML parser
  78.  
  79.   *  2005-04-25  *
  80.  
  81.  0.11 - improved skin & add link to message if no results
  82.  0.10 - bug fix - don't show iframes!
  83.  0.09 - bug fix - alert were out of scope, plus a couple visual tweaks
  84.  0.08 - bug - use (function(){ code })(); so you don't kill other javascript
  85.  0.07 - use AWS to grab amazon's prices
  86.  
  87.   *  2005-04-24  *
  88.  0.06 - wasn't checking for ISBN= in the url as well!
  89.  0.05 - added Amazon marketplace
  90.  0.04 - worked on skin to make it PURTY
  91.  0.03 - use AJAX to add the prices
  92.  0.02 - Improved interface
  93.  0.01 - initial releasea
  94.  
  95. */
  96.  
  97. // ==UserScript==
  98. // @name          Book Burro - Remixing the bookstore
  99. // @namespace     http://overstimulate.com/userscripts/
  100. // @description   Compare book prices from various book stores
  101. // @include       http://amazon.com/*
  102. // @include       http://www.amazon.com/*
  103. // @include       http://www.powells.com/*
  104. // @include       http://half.ebay.com/*
  105. // @include       http://buy.com/*
  106. // @include       http://www.buy.com/*
  107. // @include       http://biblio.com/*
  108. // @include       http://www.biblio.com/*
  109. // @include       http://search.barnesandnoble.com/*
  110. // @include       http://barnesandnoble.com/*
  111. // @include       http://www.barnesandnoble.com/*
  112. // @include       http://www.alldirect.com/*
  113. // @include       http://www.bokus.com/*
  114. // @include       http://www.internetbokhandeln.se/*
  115. // @include       http://www.akademibokhandeln.se/*
  116. // @include       http://akademibokhandeln.se/*
  117. // @include       http://www.adlibris.se/*
  118. // @include       http://*.bibl.liu.se/*
  119. // @include       http://www.cdon.com/*
  120. // @include       http://www.exlibris.se/*
  121. // @include       http://www.libris.se/*
  122. // @include       http://www.alibris.com/*
  123. // @include       http://bookbyte.com/*
  124. // @include       http://www.bookbyte.com/*
  125. // @include       http://biggerbooks.com/*
  126. // @include       http://www.biggerbooks.com/*
  127. // @include       http://ecampus.com/*
  128. // @include       http://www.ecampus.com/*
  129. // @include       http://www.booksamillion.com/*
  130. // @include       http://print.google.com/*
  131. // @include       http://www.bookpool.com/*
  132. // @include       http://bookpool.com/*
  133. // ==/UserScript==
  134.  
  135. var debug = 0;
  136.  
  137. // You are welcome either to put your own affiliate codes and dev keys below,
  138. // or to leave them as is, sponsoring further development of this application
  139. var abebooks_aid = 10395338, abebooks_pid = 1814912;
  140. var amazon_associate_code = 'diaryohayou-20';
  141. var amazon_dev_key = '05GK2S0SFN8G8P5KY482';
  142. var bn_associate_code = 41532291;
  143. var half_associate_code = 1814912; // (CJ PID, really -- not your CJ account number!)
  144.  
  145. // Of these, only name, id and bookURL are mandatory (but hostname, getISBN, ajaxURL and ajaxPrice are usually needed too).
  146. // name: book store name listed in burro window
  147. // id: a short unique id string for all document nodes related to this book store
  148. // hostname: a hostname regexp used to determine whether to look for ISBNs on this page
  149. // bookURL: address to link the name to, %s being replaced with the ISBN of the book
  150. // getISBN: function run on book store pages to find the book ISBN it's about; returns false (no book), or an ISBN string
  151. // -- or -- regexp applied to the full URL of the page, whose first matched paren pair gives the ISBN of the book shown
  152. //       -- defaults to /isbn[\/=]([0-9X]{10})(&|\?|\.|$)/i if omitted
  153. // ajaxURL: address to pick up price for the book whose ISBN is %s, for ajax handler (defaults to bookURL, if not given)
  154. // ajaxData: '' by default; if given, the data to provide with the ajax request body, %s replaced with the ISBN of the book
  155. // ajaxMethod: POST by default (if not given); which HTTP method to access the above
  156. // ajaxHeaders: HTTP headers for the AJAX request. Adds a Content-Type: 'application/x-www-form-urlencoded' header for all
  157. //              POST requests missing a content-type declaration.
  158. // ajaxPrice: function run on fetched AJAX page to get book price; returns false (not found) or a price and currency string
  159. //            or an associative array from store id to price and currency string, when one AJAX request can get many prices
  160. //   -- or -- regexp (or string form, the latter mostly useful avoid quoting /:s) whose first paren picks up the book price
  161. //   -- or -- false, in which case no xmlHTTPRequest is ever made (for instance because some other handler finds the price)
  162. // priceFix: if ajaxPrice was a string and this function is provided, the resulting price is fed through the callback
  163. // updateURL: if provided, a function run on the fetched AJAX page, to get a new (or proper) URL for the book link
  164. var handlers = [
  165.   { name: 'Abebooks.com', id: 'abebooks', hostname: /\babebooks.com$/i, getISBN: /(?:isbn|bi)=([0-9X]{10})(&|\?|$)/i,
  166.     bookURL: 'http://www.abebooks.com/servlet/SearchResults?isbn=%s&pid='+ abebooks_pid +'&aid='+ abebooks_aid,
  167.     ajaxPrice: /<span class="price">\D*(\$[^<]*)/i },
  168.   { name: 'Ad Libris', id: 'alse', hostname: /\badlibris\.se$/i,
  169.     bookURL: 'http://www.adlibris.se/shop/product.asp?isbn=%s',
  170.     ajaxMethod: 'GET',
  171.     ajaxPrice: function( html, http )
  172.     {
  173.       if( html.match( 'Ingen titel med detta ISBN finns hos AdLibris.' ) )
  174.     return '';
  175.       //return SEK( html.match( '<span class="price">([^<]*)<' )[1] );
  176.       return html.match('class="price">([^<]*)<' )[1];
  177.     } },
  178. /*
  179.   { name: 'Akademibokhandeln', id: 'abse', hostname: /\bakademibokhandeln\.se$/i,
  180.     getISBN: nextSameTagAfter( 'td', '^ISBN' ),
  181.     updateURL: firstLinkTo( 'cc_artikel.visa_artikelkort', 'http://www.akademibokhandeln.se/db/caweb/' ),
  182.     bookURL: 'http://www.akademibokhandeln.se/db/caweb/sok.avanc_artiklar?mtitel=exact&cisbn=%s',
  183.     ajaxPrice: /<span class="pris">\s*([^<]*)</i, priceFix: SEK },
  184.  
  185.   { name: 'Alibris', id: 'alibris', hostname: /\balibris\.com$/i,
  186.     getISBN: nextTagAfter( 'b', 'ISBN:' ),
  187.     bookURL: 'http://www.alibris.com/search/search.cfm', ajaxData: 'S=R&qisbn=%s',
  188.     updateURL: firstLinkTo( '/search/detail.cfm?', 'http://www.alibris.com' ),
  189.     ajaxPrice: /<span class="red-price"><b>([^<]*)</i },
  190.   { name: 'AllDirect.com', id:'alldirect', hostname: /\balldirect\.com$/i,
  191.     getISBN: nextTagAfter( 'b', 'ISBN:' ),
  192.     bookURL: 'http://www.alldirect.com/book.asp?&isbn=%s',
  193.     ajaxPrice: 'All Direct\x27s[^P]*Price[^\$]*([^<]*)<' },
  194. */
  195.   { name: 'Amazon', id: 'amazon', hostname: /\bamazon\.com$/i,
  196.     getISBN: function() {
  197.       if( location.href.match( 'rate-this' ) ) return;
  198.       return location.href.match( /\/([0-9X]{10})(\/|\?|$)/i )[1];
  199.     },
  200.     bookURL: 'http://www.amazon.com/exec/obidos/ASIN/%s/'+ amazon_associate_code,
  201.     ajaxURL: 'http://xml.amazon.com/onca/xml3?t='+ amazon_associate_code +'&dev-t='+ amazon_dev_key +'&type=lite&f=xml&mode=books&AsinSearch=%s',
  202.     ajaxPrice: function( html, http )
  203.     {
  204.       var xml = str2xml( html );
  205.       var our = xml.getElementsByTagName( 'OurPrice' );
  206.       var used = xml.getElementsByTagName( 'UsedPrice' );
  207.       return { amazon: our.length && our[0].childNodes[0].nodeValue || '',
  208.       amazon_used: used.length && used[0].childNodes[0].nodeValue || '' };
  209.     } },
  210.  
  211.   { name: 'Amazon (used)', id: 'amazon_used', ajaxPrice: false,
  212.     bookURL: 'http://www.amazon.com/exec/obidos/redirect?tag='+ amazon_associate_code +'&path=tg/stores/offering/list/-/%s/all/' },
  213. /*
  214.   { name: 'Barnes & Noble', id: 'bn', hostname: /\bbarnesandnoble\.com$/i,
  215.     bookURL: 'http://service.bfast.com/bfast/click?bfmid=2181&sourceid='+ bn_associate_code +'&bfpid=%s&bfmtype=book',
  216.     ajaxURL: 'http://search.barnesandnoble.com/booksearch/isbninquiry.asp?isbn=%s',
  217.     ajaxPrice: 'priceRightBNPrice[^>]*>([^<]*)</' },
  218. */
  219.   { name: 'BestPrices.com', id: 'bestprices', hostname: /\bbestprices\.com$/i,
  220.     bookURL: 'http://www.bestprices.com/cgi-bin/vlink/' + bn_associate_code,
  221.     getISBN: function() {
  222.       return location.href.match( /vlink\/(.*)/i )[1];
  223.     },
  224.     ajaxURL: 'http://www.bestprices.com/cgi-bin/vlink/%s',
  225.     ajaxPrice: /Our Price:[^\$]*([^<]*)</,
  226.     ajaxMethod: 'GET'
  227.   },
  228.  
  229.   { name: 'Biblio.com', id: 'biblio', hostname: /\bbiblio\.com$/i,
  230.     bookURL: 'http://www.biblio.com/isbn/%s.html', ajaxMethod: 'GET',
  231.     //ajaxPrice: /<div[^>]*>\s*((?:$|$)\S+)\s*</i 
  232.     ajaxPrice: /<div[^>]*>\s*$([^<]*)/i 
  233.   },
  234.  
  235.   { name: 'BiggerBooks.com', id: 'biggerbooks', hostname: /\bbiggerbooks\.com$/i,
  236.     bookURL: 'http://www.dpbolvw.net/click-'+ half_associate_code +'-9467039?ISBN=%s',
  237.     ajaxURL: 'http://www.biggerbooks.com/bk_detail.asp?ISBN=%s', ajaxMethod: 'GET', 
  238.     //ajaxPrice: /<span class='price'>([^<]+)/ 
  239.     ajaxPrice: /Our Price <span class='price'>([^<]+)/
  240.   },
  241. /*
  242.   { name: 'Bokus.com', id: 'bokus', hostname: /\bbokus\.com$/i,
  243.     getISBN:/(?:\/|FAST_VALUE=ISBN&FAST=|ISBN=)([0-9X]{10})(\.html)?(\?|&|$)/i,
  244.     bookURL: 'http://www.bokus.com/b/%s.html', 
  245.     ajaxURL: 'http://www.bokus.com/cgi-bin/book_search.cgi?FAST_VALUE=ISBN&FAST=%s',
  246.     ajaxPrice: '<span class="price"[^>]*>([^<]*)<', ajaxMethod: 'GET'
  247.     //, priceFix: SEK 
  248.   },
  249. */
  250.   { name: 'Bookbyte', id: 'bookbyte_new', hostname: /\bbookbyte\.com$/i,
  251.     //getISBN: function(){ return document.getElementById('lbIsbn').textContent; },
  252.     bookURL: 'http://www.kqzyfj.com/click-'+ half_associate_code +
  253.          '-10365617?url=http%3A%2F%2Fwww.bookbyte.com%2Fproduct.aspx%3Fisbn%3D%s',
  254.     ajaxURL: 'http://www.bookbyte.com/product.aspx?isbn=%s', 
  255.     ajaxMethod: 'GET',
  256. /*
  257.     ajaxPrice: function( html, http ) {
  258.       var prices = { bookbyte_new: html.match( '<span id="lbNewPrice">(?:<[^>]*>)*([^<]*)' ),
  259.              bookbyte_used: html.match( '<span id="lbUsedPrice">(?:<[^>]*>)*([^<]*)' ),
  260.              bookbyte_bazaar: html.match( '<a href="#Bazaar_Bookmark" class=price-red-14>([^<]*)' ) }, i;
  261.       for( i in prices )
  262.     prices[i] = prices[i] ? unHTML( prices[i][1] ).replace( /[^$.0-9]/g, '' ) : '';
  263.       return prices;
  264.     }
  265. */
  266.     ajaxPrice: '<span id="ctl00_ContentPlaceHolder1_lbNewPrice">New <a class="one"[^>]*>([^<]*)<'
  267.   },
  268. /*
  269.   { name: 'Bookbyte (used)', id: 'bookbyte_used', ajaxPrice: false, bookURL: 'http://www.kqzyfj.com/click-'+ 
  270.     half_associate_code +'-10365617?url=http%3A%2F%2Fwww.bookbyte.com%2Fproduct.aspx%3Fisbn%3D%s' },
  271.   { name: 'Bookbyte (bazaar)', id: 'bookbyte_bazaar', ajaxPrice: false, bookURL: 'http://www.kqzyfj.com/click-'+ 
  272.     half_associate_code +'-10365617?url=http%3A%2F%2Fwww.bookbyte.com%2Fproduct.aspx%3Fisbn%3D%s' },
  273. */
  274.   { name: 'Bookpool.com', id: 'bookpool', hostname: /\bbookpool.com$/i,
  275.     getISBN: /(?:qs|is|sm)[\/=]([0-9X]{10})(&|\?|\.|$)/i,
  276.     bookURL: 'http://www.bookpool.com/sm/%s',
  277.     ajaxPrice: /Our\s+Price:?(?:<[^>]*>\s*)*([^<]+)/i },
  278.   { name: 'Booksamillion.com', id: 'bamm', hostname: /\bbooksamillion\.com$/i,
  279.     bookURL: 'http://www.dpbolvw.net/click-'+ half_associate_code +'-42121?isbn=%s', // /<B>Our\s+Price:\s*([^<]+)/i
  280.     ajaxURL: 'http://www.booksamillion.com/ncom/books?type=isbn&find=%s', ajaxMethod: 'GET',
  281.     ajaxPrice: function( html, http ){ return '$' + html.match( /Our\s+Price:\s*\$?([^<]+)/i )[1]; } },
  282.   { name: 'Buy.com', id: 'buy', hostname: /\bbuy\.com$/i,
  283.     getISBN: function(){
  284.     var s = document.title.match( /ISBN ([0-9X]{10}) -/i )[1];
  285.     if(!s) s = document.title.match( /ISBN ([0-9]{13}) -/i )[1]; 
  286.     return s;
  287.     },
  288.     bookURL: 'http://www.buy.com/retail/GlobalSearchAction.asp?qu=%s',
  289.     //ajaxPrice: 'productPrice[^>]*>([^<]*)</' 
  290.     ajaxPrice: 'Our Price:[^\$]*([^<]*)</' 
  291.   },
  292. /*
  293.   { name: 'CDON.com', id: 'cdon', hostname: /\bcdon\.com$/i,
  294.     getISBN: nextSameTagAfter( 'td', 'ISBN:' ), updateURL: function( html, http ) {
  295.       return 'http://www.cdon.com/' + unHTML(html.match( /<a href=.(product\.phtml\?prod=\d+)/ )[1]);
  296.     }, bookURL: 'http://www.cdon.com/search_result.phtml?ed_search_value=%s&sl_search_field=books_isbn',
  297.     ajaxPrice: '<td[^>]* class=\'priceMedium\'[^>]*>([^<]*)<', ajaxMethod: 'GET', priceFix: SEK },
  298. */
  299.   { name: 'eCampus.com', id: 'ecampus', hostname: /\becampus.com$/i,
  300.     bookURL: 'http://www.anrdoezrs.net/click-'+ half_associate_code +'-5029466?ISBN=%s',
  301.     ajaxURL: 'http://www.ecampus.com/bk_detail.asp?ISBN=%s', 
  302.     ajaxMethod: 'GET',
  303.     //ajaxPrice: /Our\s+Price\s*<font[^>]*>([^<]+)/i 
  304.     ajaxPrice: /New Price[^\$]*([^<]*)</i 
  305.   },
  306. /*
  307.   { name: 'Ex Libris', id: 'exlibris', hostname: /\bexlibris\.se$/i,
  308.     getISBN: nextSameTagAfter( 'span', 'ISBN:' ), updateURL: function( html, http ) {
  309.       return 'http://www.exlibris.se/' + unHTML(html.match( '<form [^>]* action="([^"]*)' )[1]);
  310.     }, bookURL: 'http://www.exlibris.se/advancedsearch.aspx?%s', ajaxMethod: 'GET',
  311.     ajaxURL: 'http://www.exlibris.se/searchresult.aspx?TYPE=Simple&SEARCH_FIELD=2&SEARCH_FIELD_TEXT=ISBN&SEARCH_VALUE=%s&SHOW_IMAGES=true',
  312.     ajaxPrice: /<span id="ctrlProductDetails_lblPrice">([^<]+)</, priceFix: SEK 
  313.   },
  314.  
  315.   { name: 'Google Print', id: 'googleprint', hostname: /print\.google\.com$/i,
  316.     bookURL: 'http://print.google.com/print?as_isbn=%s',
  317.     getISBN: function() {
  318.       var re = new RegExp( 'https?://www.google.com/pagead/ads\\?.*' +
  319.                '&channel=[^&]*BTB-ISBN:([0-9X]{10})', 'i' );
  320.       return firstDocumentLinkMatching( re, 1 );
  321.     }, updateURL: firstLinkTo( 'http://print.google.com/print?', '' ),
  322.     ajaxPrice: function( html, http ) {
  323.       var re = new RegExp( '<a(?:[^>]*) href=["\']?' +
  324.                'https?://print.google.com/print\\?' +
  325.                '[^"\']*&sig=', 'i' );
  326.       return html.match( re ) != null;
  327.     } },
  328.   { name: 'Half.com', id: 'half', hostname: /\bhalf\.ebay\.com$/i,
  329.     getISBN: nextTagAfter( 'b', 'ISBN:' ),
  330.     bookURL: 'http://www.kqzyfj.com/click-'+ half_associate_code +'-1932276?ISBN=%s',
  331.     ajaxURL: 'http://half.ebay.com/search/search.jsp?product=books:isbn&query=%s',
  332.     ajaxPrice: 'Best[^P]*Price[^$]*([^<]*)<' },
  333.  
  334.   { name: 'Internetbokhandeln', id: 'ibse', hostname: /\binternetbokhandeln\.se$/i,
  335.     getISBN: /(?:\/bok|&s_search=)([0-9Xx]{10})(\.html)?(\?|$)/, updateURL: function( html, http )
  336.     {
  337.       return 'http://www.internetbokhandeln.se' + unHTML(html.match('<a href="?(/[^"]*FIXME/bok[0-9xX]{10}\\.html)')[1]);
  338.     }, bookURL: 'http://www.internetbokhandeln.se/_114K3FPUVX/msearchres.html?s_sortby=b&s_media=ALL&s_type=i&s_search=%s',
  339.     ajaxMethod: 'GET', ajaxPrice: '<span class=price>([^<]*)<', priceFix: SEK },
  340.  
  341.   { name: 'Libris Media', id: 'librismedia', hostname: /\blibris\.se$/i,
  342.     getISBN: function(){ return document.body.innerHTML.match( /<span class=[^>]*>\s*ISBN ([0-9X]{10})/mi )[1]; },
  343.     updateURL: firstLinkTo( 'Browse_Item_Details.asp', 'http://www.libris.se/stores_app/' ),
  344.     bookURL: 'http://www.libris.se/stores_app/Browse_dept_items.asp?Store_id=102&fran=sok',
  345.     ajaxData: 'SEARCH_SKU=%s&SEARCH_DEPT=-1&Search_Store.x=36&Search_Store.y=7',
  346.     ajaxPrice: [/<span class="normal2">Pris:.*?<span[^>]*>(\d+)(,\d\d)? *kr</mi,
  347.         /<b>\s*Libris l\345gpris (\d+)(,\d\d)? *kr/mi,
  348.         /<span class="fakta">\s*(\d+)(,\d\d)? *kr/mi], priceFix: SEK },
  349.   { name: 'LiU Library', id: 'liubibl', hostname: /\bbibl\.liu\.se$/i,
  350.     getISBN: nextSameTagAfter( 'a', 'ISBN/ISSN:' ),
  351.     bookURL: 'http://hip.bibl.liu.se/ipac20/ipac.jsp?menu=search&index=ISBN&term=%s',
  352.     ajaxPrice: function( html, http )
  353.     {
  354.       var book, inne = 0, ute = 0;
  355.       while( book = /<tr height="15">(.*?)<\/tr>/gi.exec( html ) )
  356.     if( !book[1].match( /referens/i ) )
  357.       if( book[1].match( /tillg|reserverad/i ) )
  358.         inne++;
  359.       else
  360.         ute++;
  361.       if( inne+ute )
  362.       {
  363.     html = inne;
  364.     if( ute )
  365.       return html + ' (of '+(inne+ute)+')';
  366.     return html + ' in';
  367.       }
  368.     } },
  369. */
  370.   { name: 'Powell\'s Books', id: 'powells', hostname: /\bpowells.com$/i,
  371. /*
  372.     getISBN: function()
  373.     {
  374.       var dt = document.getElementsByTagName( 'dt' ), i;
  375.       for( i=0; i<dt.length; i++ )
  376.     if( dt[i].innerHTML.match( 'ISBN:' ) &&
  377.         checkISBN( dt[i].nextSibling.title ) )
  378.       return dt[i].nextSibling.title;
  379.     },
  380.     getISBN: function()
  381.     {
  382.     return location.href.match( /isbn=(.*)/i )[1]);
  383.     },
  384. */
  385. //http://www.powells.com/cgi-bin/biblio?inkey=18-9780545010221-0
  386. //http://www.powells.com/cgi-bin/biblio?isbn=0596008732
  387.     getISBN: function()
  388.     {
  389.     return  location.href.match(/inkey=[^\-]*-([^\-]*)-/)[1] ||
  390.         location.href.match(/isbn=(.*)/)[1] ||
  391.         location.href.match(/ISBN=(.*)/)[1];
  392.     },
  393.     bookURL: 'http://www.powells.com/cgi-bin/biblio?isbn=%s',
  394.     ajaxPrice: '<div class="price">([^<]*)<' 
  395.   }
  396. ];
  397.  
  398. var hasFetched = false;
  399. var Icons =
  400. {
  401.   title: 'data:image/gif;base64,R0lGODlheAAOAOYAAAAAAOmUUUOGfx1eVj8/M1ijnqBhLrmFVv//zNi6oCUIAO/vwJqEbGk4EL+/mm9vWZmZmVpaWvfAl7ydiC8vJo+PcwAPCaJtSylNS////4tCC9/fs0MeAa+vjefy9LR1QpnMzH9/Zny3s7WIazttbG5MMVyVj09PQJ+fgMvO11UwEwMWGq10R5K6ugg2Nh8fGdiecOaxhdiKTAAACZlqPnWtqu/dxTxlYXNeS1o6IU8rEZqytlp2d4p4bDZ/dn+Eh4pWL2JNOFKLhf/frtusjtGFS750N8ulfo1qVM/Ppm2HhsSQa615UvSlYy5HS9rb1hAEDqdvPjgpJg8PDP/vx4S2s6toMzRERKmlmrS8vkFfXwAhHnI7Ek06K+3x8D92c6/M0DNmZk5nb4xNGr+8s9uNT8x9OysRAtSniJWOlq6GYwAICZGtsP+4dGVCLkaVj4GpqF9fTMuLW8q/ra1rOV1UTXpKJmaZmb+SabGzstWXZEtMTmg9G4RZN4rFwVZeYSH5BAUUAAgALAAAAAB4AA4AAAf/gAiCg4MzM4SIgyo5hGcKj1JTiZOUlZaXmJmagmtrPEprlRN9TA0ICjpIaiMjSEhdm7Gys7SInXd3JiQ3oZN8Bx9jXGpoaEtLFzRumkkEKLTNz4nRtbWGbHBVJttCJBaTDQYaYzASSzAHQCxEmw4AIbTu8Iny1bMzWTtwcH4Fbz4DBHhLZGfMmCJy5NCwY0qWuwdxCHQYVIHAiYmCkkR8sABBvQ4hkgx6GBFjCGknPYYkUCGEgwcnNix4QCCOyEsrVoBpUUOEvwIAw4T5RkjFmA8faPDhU8sdgAcUADhA8ODpCQATN0yhEGKrx3coAMQh5BSqVAQACAhKiyAEgCnv/94CAICAwpQQdjdcmiFmRQuf/nwICDMAA4ZeCLrouBClMR8VJUrMcjd2A4CYlwVNeYEgDgCRFQCgcHdVLVmxCCyfQGuardvVaD8jCFsBQRLUllY40eICRA0BwL8I7ZRhUA8VNAx8sIOGSoISy2LVYz2dAF3rguQ5xT3y3Vq1bFm3PSt+/NTyliyIwbAFhB+BJIinKC5oAh8WBqzAoEKFA5ISONTxRwqZPITAApfdNhYCL0hylV60uUNBVedl99SBmYXnGnnhhfaMapmsgYEY7bWwxRoZZJBCGlAMMgEQdMhghR5DxKAADWokYMMTWeyBiTtTdODZM3Z1EBo8HYjlgL9dC8izwBSmWRjkkAhMMeWG54WnFQUOeFbhJWtoocWJHqj4gyGE6MBCGQHIUIQZetBwAB5qBEGGF3n8KNYLFx541RQPDIICnxSIVE9Y0nwVB5+BzgbXCRSo5VaWUSYR1QuJhniFBRlkAQEAiA1yBgtFtNGEEUbQYUAUeKDBABYZPBGBPbTWWs1cP4BKyRk06NFEAKdawUIUTBBhgxfI+mjrssxiMpclXdhBRxlmmJEUDTQckcAcXnjwRKPNhltrIAA7',
  402.   about: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAuJAAALiQE3ycutAAAAB3RJTUUH1QQYCDkprC+64gAAAOdJREFUKM+FkrFOAkEURc9ddrJZCgsqCwtCIiUFJa2df2CwNXRS8AmGisLEaGGs3VhTYDUtxZaED/AriGHjPhuEDC5wq3l5czL3vjeyzBlHtC6KoI4BuPk8Qqx3549rov3+YPBIp3NHq3VLlnkkBf0AWK2+6fevWCze8H7CaPRKUfzgnKsG0jSh272kLEuWyy/a7QvSNKnIsJEk6vWE2SxnPH5nOn3YWopjh9VqIbDBGA5fyPNnGo2znZVIEEWhJTNDgmbzfHvZzA6H/pP3k3/TqQQkIYle7z6oT74wnz8d3KNOfY19/QKFiTrWqbiPtAAAAABJRU5ErkJggg==',
  403.   carrotRight: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAALCAYAAACprHcmAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH1QQYCR020Q08hgAAAB10RVh0Q29tbWVudABDcmVhdGVkIHdpdGggVGhlIEdJTVDvZCVuAAAAiklEQVQY07XPIQoCURSF4e8NAzYxGicNuAa1WlyCO3AlZnfiNgwahQFxikkcBIsGfaZpzgODJ/4c/nMvPyR8g7EsephgH6q6aXnWIelhjkUsi0EL88TqFUfMYlnscMoS5wUccMYS4yxhfuGNPho88oQ5xxQjrHHpKkcMccMqVPU99eATG2zb4n/zAS4OHrV1hIB/AAAAAElFTkSuQmCC',
  404.   carrotDown: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAALCAYAAACprHcmAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH1QQYCRoeq/kCuwAAAB10RVh0Q29tbWVudABDcmVhdGVkIHdpdGggVGhlIEdJTVDvZCVuAAAAmElEQVQY083QMUoDARSE4W92U2xhkxSpAgY0AXMQ29zCM3iSXEXQTrCws44Im85CkO1jnq2KKQWnGxiY+Yd/oXw1tTrr7D86CYdD5Xk3HA8vTi8la7xW1T5Jg8IEN6PvPXnEAkOa5l5Viwuc4yk/d9VyfoLrqnpIMmCGu2z79/wGUsv5FFcYY5Nt/wKjI+BvuEWnbfu///kTargo75QVC5oAAAAASUVORK5CYII=',
  405.   close: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAuJAAALiQE3ycutAAAAB3RJTUUH1QQYCDcSg6d+SAAAAPBJREFUKM+Fkr1qAkEURs9dnWBrJT5IHkEw4gtEsBQs/AlpkiKlJGnEJlqIjRbrPoAQYhPio1hGsPAHFoW5KSIxo7J7qvlgDty534j6Rolgt987OQnA7XuEsTuegwIeMYiIkx2hVnsjCL7+su9/0mz2Lox0oNOpUiw+kc2mUVWGww8mkxYiYK09F4xJMho9kMs9IiJMpy8Y83vFWkUTCVcAWCxWLJcrRIT1OiSTOczuCXieK2y3IeXyK4PBPZtNSKn0zGzWJpW6uvyGer1LpVIgn78GYD7/ptHo0e/fHbemvtHIHv4zvonv4ayXuK9xyg8qt0tfe9qKPAAAAABJRU5ErkJggg=='
  406. };
  407.  
  408. function quoteRegExp( re )
  409. {
  410.   return re.replace( /([.*+^$?(){}\[\]\\])/g, '\\$1' );
  411. }
  412.  
  413. function regexpify( stringOrRegExp, flags, quote )
  414. {
  415.   if( (typeof stringOrRegExp == 'function') && stringOrRegExp.exec )
  416.     return stringOrRegExp;
  417.   if( quote ) stringOrRegExp = quoteRegExp( stringOrRegExp );
  418.   return new RegExp( stringOrRegExp, flags );
  419. }
  420.  
  421. function firstDocumentLinkMatching( re, paren )
  422. {
  423.   var x, i;
  424.   for( i=0; i<document.links.length; i++ )
  425.     if( (x = re.exec( document.links[i].href )) )
  426.       return x[paren]; // should perhaps backtrack up to parent TR, get its
  427.   // previous sibling and verify that its contentText == 'Buy this Book'
  428. }
  429.  
  430. // AJAX callback returning the first <a href="..."> link to a page whose URL starts with `url'.
  431. // baseURL is prepended to the returned result, so passing "/search?" and "http://example.com"
  432. // might yield a link to "http://example.com/search?isbn=0596000480", if the page contained it.
  433. function firstLinkTo( url, baseURL )
  434. {
  435.   return function( html, http )
  436.   {
  437.     return baseURL + unHTML(html.match( '<a(?:[^>]*) href=["\']?('+ quoteRegExp( url ) +'[^\'" >]*)' )[1]);
  438.   };
  439. }
  440.  
  441. function nextTagAfter( tag, content )
  442. {
  443.   return function()
  444.   {
  445.     var tags = document.getElementsByTagName( tag ), i, node, isbn;
  446.     for( i=0; i<tags.length; i++ )
  447.       if( tags[i].innerHTML.match( content ) )
  448.     for( j=tags[i].nextSibling; j=j.nextSibling; )
  449.       if( (isbn = j.textContent.replace( /-/g, '' ).match( /[0-9X]{10}/i )) )
  450.         return isbn[0];
  451.   };
  452. }
  453.  
  454. function nextSameTagAfter( tag, content )
  455. {
  456.   return function()
  457.   {
  458.     var isbn, i, tags = document.getElementsByTagName( tag );
  459.     for( i=0; i<tags.length-1; i++ )
  460.       if( tags[i].innerHTML.match( content ) &&
  461.       (isbn = tags[i+1].innerHTML.match( /[0-9X]{10}/i )) &&
  462.       (isbn = checkISBN( isbn && isbn[0] )) )
  463.     return isbn;
  464.   };
  465. }
  466.  
  467. function unHTML( html )
  468. {
  469.   return html.replace( /&(amp|lt|gt|quot|apos|#(\d+));/g, function( match, character, code )
  470.   {
  471.     return { amp:'&', lt:'<', gt:'>', quot:'"', apos:'\'' }[character] || String.fromCharCode( code );
  472.   });
  473. }
  474.  
  475. function SEK( price ){ return price.replace( /:-|kr| /gi, '' ) +' SEK'; }
  476.  
  477. // document.getElementById() on steroids (ask for several ids, and you get them back as an array)
  478. function $( ids, doc )
  479. {
  480.   if( typeof ids == 'string' )
  481.     return (doc || document).getElementById( ids );
  482.   for( var i=0; i<ids.length; i++ )
  483.     ids[i] = (doc || document).getElementById( ids[i] );
  484.   return ids;
  485. }
  486.  
  487. // price cache since last seen book, one day expiry time
  488. var last_isbn = PRO_getValue( 'last_isbn', '' );
  489. //PRO_setValue( 'last_run', '0' );
  490. var last_run = PRO_getValue( 'last_run', '0' );
  491. var last_prices = PRO_getValue( 'last_prices', '' );
  492. //alert( 'last_run:'+last_run+'\nsaved:\n\n' + last_prices );
  493.  
  494. function getPrices( isbn )
  495. {
  496.   var now = (new Date).getTime();
  497.   //alert( isbn +':'+ last_isbn +'\n'+ (now-parseInt( last_run )) +'\n' + last_prices );
  498.   if( (isbn != last_isbn) || (now-parseInt( last_run ) > 36e5) ||
  499.       (last_prices.split( '\n' ).length != handlers.length) ) return false;
  500.   return decodePrices( last_prices );
  501. }
  502.  
  503. var prices = {};
  504. function updateCache( isbn, store, price )
  505. {
  506.   var now = (new Date).getTime(), got = 0, i, h;
  507.   if( (isbn != last_isbn) || (now - parseInt( last_run ) > 864e5) )
  508.     prices = {};
  509.   last_isbn = isbn;
  510.   last_run = now.toString();
  511.   prices[store] = price;
  512.   for( i in prices ) got++;
  513.   if( got == handlers.length )
  514.   {
  515.     //alert( encodePrices( prices ) );
  516.     PRO_setValue( 'last_prices', last_prices = encodePrices( prices ) );
  517.     PRO_setValue( 'last_isbn', isbn );
  518.     PRO_setValue( 'last_run', last_run );
  519.   }
  520.   else
  521.     ;//alert( got +'/' + handlers.length );
  522. }
  523.  
  524. function decodePrices( stored )
  525. {
  526.   var prices = {}, i, raw = (stored||'').split( '\n' );
  527.   for( i=0; i<raw.length; i++ )
  528.   {
  529.     var data = raw[i].split( ':' );
  530.     prices[data.shift()] = data.join( ':' );
  531.   }
  532.   return prices;
  533. }
  534.  
  535. function encodePrices( prices )
  536. {
  537.   var i, raw = [];
  538.   for( i in prices )
  539.     raw.push( i +':'+ prices[i] );
  540.   return raw.join( '\n' );
  541. }
  542.  
  543. function updatePrices( prices, isbn )
  544. {
  545.   var id, node, price, data, errmsg = 
  546.     'Oops! Either there are no books available,\\n' +
  547.     'or there is a parsing error due to changes\\n' +
  548.     'in to this website\\\'s page format.';
  549.   for( id in prices )
  550.     if( (node = document.getElementById( 'burro_'+id )) )
  551.     {
  552.       if( debug ) PRO_log( id +' price set to '+ prices[id] );
  553.       data = (prices[id] || '').split( ':' );
  554.       if( (price = data.shift()) )
  555.       {
  556.     node.firstChild.nodeValue = price;
  557.     if( data.length )
  558.       updateURL( id, data.join( ':' ) );
  559.       }
  560.       else
  561.     node.innerHTML = '<a style="text-decoration:none;color:#00A;" href="javascript:alert(\''+ errmsg +'\');">none</a>';
  562.       if( isbn )
  563.     updateCache( isbn, id, price );
  564.     }
  565. }
  566.  
  567. function findHeader( headers, name )
  568. {
  569.   var i, found;
  570.   name = regexpify( name, 'i', 'quote' );
  571.   for( i in headers )
  572.     if( i.match( name ) )
  573.       return headers[i];
  574. }
  575.  
  576. function runQueries( isbn )
  577. {
  578.   var j, request, callback, prices = debug ? false : getPrices( isbn );
  579.   if( prices ) return updatePrices( prices ); // already cached
  580.   for(var i=0; i<handlers.length; i++ )
  581.   {
  582.     var h = handlers[i];
  583. PRO_log('Query ' + h.id + ' for isbn ' + isbn);
  584.     if( h.ajaxPrice != false )
  585.     {
  586.       if( debug )
  587.     PRO_log( h.id +': '+ (h.ajaxMethod||'POST')+'( '+(h.ajaxURL||h.bookURL) + ' )' );
  588. /*
  589.       request = { method:h.ajaxMethod||'POST', data:(h.ajaxData||'').replace( /%s/g, isbn ),
  590.           onload:makeAjaxCallback( h, isbn ), url:(h.ajaxURL||h.bookURL).replace( /%s/g, isbn ) };
  591.       if( h.ajaxHeaders ) request.headers = h.ajaxHeaders;
  592.       if( (request.method == 'POST') && !findHeader( request.headers = request.headers || {}, 'Content-Type' ) )
  593.     request.headers['Content-Type'] = 'application/x-www-form-urlencoded';
  594.       PRO_xmlhttpRequest( request );
  595. */
  596.       go(h, isbn);
  597.     }
  598.   }
  599. }
  600.  
  601. function go(h, isbn)
  602. {
  603.       var xmlhttp = PRO_xmlhttpRequest();
  604.       var method = h.ajaxMethod || 'POST';
  605.       var url = (h.ajaxURL||h.bookURL).replace( /%s/g, isbn );
  606.       var data = (h.ajaxData||'').replace( /%s/g, isbn );
  607.       var headers = h.ajaxHeaders;
  608.  
  609.       xmlhttp.open(method, url, true);
  610. /*
  611.       if(method == 'POST' && !findHeader( headers || {}, 'Content-Type' ) )
  612.     xmlhttp.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
  613. */
  614.  
  615.       if(method == 'POST') xmlhttp.send(data);
  616.       else xmlhttp.send(null);
  617.       xmlhttp.onreadystatechange = function(){
  618.     if(xmlhttp.readyState == 4) makeAjaxCallback(h, isbn)(xmlhttp);
  619.       }
  620. }
  621.  
  622. function updateURL( id, url )
  623. {
  624.     document.getElementById( 'burro_book_' + id ).href = url;
  625. }
  626.  
  627. function makeAjaxCallback( handler, isbn)
  628. {
  629.   var callback = handler.ajaxPrice;
  630.   if(debug) PRO_log('Callback for ' + handler.name + ':' + (typeof callback));
  631.   switch( typeof callback )
  632.   {
  633.     case 'function':
  634.       if( !callback.exec )
  635.     break; // callback is already our proper callback
  636.       // fall-through -- it was a regexp:
  637.  
  638.     case 'string':
  639.       callback = function( html, http )
  640.       {
  641.     var x = html.match( handler.ajaxPrice );
  642.     var price = html.match( handler.ajaxPrice )[1].replace( / /g, '' );
  643.     if( debug > 2 ) PRO_log( 'found raw '+ handler.id +'price '+ price );
  644.     if( price ) price = unHTML( price );
  645.     if( price && handler.priceFix ) price = handler.priceFix( price );
  646.     return price;
  647.       };
  648.       break;
  649.  
  650.     case 'object': // array of string or regexp
  651.       callback = function( html, http )
  652.       {
  653.     var i, price, regexps = handler.ajaxPrice;
  654.     if(regexps.length){
  655.       for( i=0; i<regexps.length; i++ )
  656.         if( (price = html.match( regexps[i] )) )
  657.         {
  658.           price = price[1];
  659.           if( debug > 2 ) PRO_log( 'found raw '+ handler.id +' price '+ price +' for regexp '+ regexps[i] );
  660.           if( price && handler.priceFix ) price = handler.priceFix( price );
  661.           return price;
  662.         }
  663.           } else {
  664.         if( (price = html.match(handler.ajaxPrice)) )
  665.         {
  666.           price = price[1];
  667.           if( price && handler.priceFix ) price = handler.priceFix( price );
  668.           return price;
  669.             }
  670.           }
  671.       };
  672.       break;
  673.  
  674.     default:
  675.       alert( 'Price handler for '+ handler.name +' (id '+ handler.id +
  676.          ') is of illegal type '+ (typeof callback) +'! Ignoring.' );
  677.       // fall-through:
  678.  
  679.     case 'boolean':
  680.       callback = function(){};
  681.   }
  682.   return function( xmlHttpResponse )
  683.   {
  684.     if( debug ) with( xmlHttpResponse )
  685.       PRO_log( 'Status ' + status+' '+statusText + ': '+ responseText.length +' bytes' );
  686.     var failure = false, prices = {}, result = '', node, id, price;
  687.     try {
  688.       result = callback( xmlHttpResponse.responseText, xmlHttpResponse ) || '';
  689.     } catch (e) {
  690.       failure = true;
  691.       if( debug )
  692.     PRO_log( 'Failed to load or parse '+ handler.name +' (id '+ handler.id +'): ' +e );
  693.  
  694.     }
  695.  
  696.     //if( debug && handler.id == 'ecampus')
  697.     PRO_log( 'Response from ' + handler.id + ':' + xmlHttpResponse.responseText );
  698.  
  699.  
  700.     if( handler.updateURL )
  701.     try {
  702.       var url = handler.updateURL( xmlHttpResponse.responseText, xmlHttpResponse );
  703.       if( url )
  704.     updateURL( handler.id, url );
  705.     } catch( e ) {}
  706.     if( typeof result == 'string' ) // a price tag
  707.     {
  708.       if( url ) result += ':' + url; // a new book URL to link
  709.       prices[handler.id] = result;
  710.     }
  711.     else if( typeof result == 'boolean' ) // availability info only
  712.     {
  713.       if( url ) result += ':' + url; // a new book URL to link
  714.       prices[handler.id] = result ? 'available' : 'unavailable';
  715.     }
  716.     else
  717.       prices = result;
  718.     updatePrices( prices, isbn );
  719.   };
  720. }
  721.  
  722. function checkEAN( ean )
  723. {
  724.   try {
  725.     ean = (ean||'').replace( /-| | /gi, '' );
  726.     if( ean.length != 13 ) return false;
  727.     var checksum = 0;
  728.     for( var i=0; i<12; i++ )
  729.       checksum += ean.charAt(i) * (i&1 ? 3 : 1);
  730.     checksum = (10 - (checksum % 10)) % 10;
  731.     if( ean.charAt(12) == checksum )
  732.       return ean;
  733.     else
  734.     {
  735.       if( debug ) PRO_log( 'EAN '+ean+ ' failed checksum.' );
  736.       return false;
  737.     }
  738.   }
  739.   catch (e)
  740.   { 
  741.     if( debug ) PRO_log( 'checkEAN: '+ e );
  742.     return false;
  743.   }
  744. }
  745.  
  746. function checkISBN( isbn )
  747. {
  748.   try {
  749.     isbn = (isbn||'').toLowerCase().replace( /-| | /g, '' );
  750.     if( isbn.length == 13) return true;
  751.     if( isbn.length != 10 ) return false;
  752.     var checksum = 0;
  753.     for( var i=0; i<9; i++ )
  754.       if( isbn.charAt(i) == 'x' )
  755.         checksum += 10 * (i+1);
  756.       else
  757.         checksum += isbn.charAt(i) * (i+1);
  758.     checksum = checksum % 11;
  759.     if( checksum == 10 ) checksum = 'x';
  760.     if( isbn.charAt(9) == checksum )
  761.       return isbn;
  762.     else
  763.     {
  764.       if( debug ) PRO_log( 'ISBN '+isbn+ ' failed checksum.' );
  765.       return false;
  766.     }
  767.   }
  768.   catch (e)
  769.   { 
  770.     if( debug ) PRO_log( 'checkISBN: '+ e );
  771.     return false;
  772.   }
  773. }
  774.  
  775. function dom_createLink( url, txt, title, id )
  776. {
  777.   var a  = document.createElement( 'a' );
  778.   if( id ) a.id = id;
  779.   a.setAttribute( 'target', '_top' );
  780.   a.setAttribute( 'href', url );
  781.   with( a.style )
  782.   {
  783.     color = '#00A';
  784.     textDecoration = 'none';
  785.     fontWeight = 'bold';
  786.   }
  787.   if( title ) a.setAttribute( 'title', title );
  788.   a.appendChild( document.createTextNode( txt ) );
  789.   return a;
  790. }
  791.  
  792. function addSite( url, title, loc_id )
  793. {
  794.   var tr = document.createElement( 'div' );
  795.   var td_left = document.createElement( 'span' );
  796.   var a = dom_createLink( url, title, title+' Search', 'burro_book_'+loc_id );
  797.   td_left.style.paddingLeft = '5px';
  798.   td_left.appendChild( a );
  799.   tr.appendChild( td_left );
  800.  
  801.   var td_right = document.createElement( 'span' );
  802.   td_right.innerHTML = 'fetching';
  803.   td_right.style.paddingRight = '5px';
  804.   td_right.id = 'burro_' + loc_id;
  805.   tr.appendChild( td_right );
  806.  
  807.   if( document.all ) // IE only
  808.   {
  809.     tr.style.position = 'relative';
  810.     td_right.style.textAlign = 'right';
  811.     td_right.style.position = 'absolute';
  812.     td_right.style.left = '10em';
  813.     td_right.style.width = '4em';
  814.   }
  815.   else // other browsers
  816.   {
  817.     tr.style.display = 'table-row';
  818.     td_left.style.display = 'table-cell';
  819.     td_right.style.display = 'table-cell';
  820.   }
  821.   return tr;
  822. }
  823.  
  824. function str2xml( strXML )
  825. {
  826.   if( window.ActiveXObject )
  827.   {
  828.     var domdoc = new ActiveXObject( 'Microsoft.XMLDOM' );
  829.     domdoc.async = 'false';
  830.     domdoc.loadXML( strXML );
  831.     return domdoc;
  832.   }
  833.   else
  834.   {
  835.     var objDOMParser = new DOMParser();
  836.     return objDOMParser.parseFromString( strXML, 'text/xml' );
  837.   }
  838. }
  839.  
  840. function int2money( cents )
  841. {
  842.   var money = '$'
  843.   if( cents < 100 )
  844.     money = money + '0.';
  845.   else
  846.     money = money + Math.floor( cents/100 ) + '.';
  847.   cents = cents % 100;
  848.   if( cents < 10 )
  849.     money = money + '0';
  850.   money = money + cents;
  851.   return money;
  852. }
  853.  
  854. var Drag = function(){ this.init.apply( this, arguments ); };
  855.  
  856. Drag.fixE = function( e )
  857. {
  858.   if( typeof e == 'undefined' ) e = window.event;
  859.   if( typeof e.layerX == 'undefined' ) e.layerX = e.offsetX;
  860.   if( typeof e.layerY == 'undefined' ) e.layerY = e.offsetY;
  861.   return e;
  862. };
  863.  
  864. Drag.prototype.init = function( handle, dragdiv )
  865. {
  866.   this.div = dragdiv || handle;
  867.   this.handle = handle;
  868.   if( isNaN(parseInt(this.div.style.right )) ) this.div.style.right  = '0px';
  869.   if( isNaN(parseInt(this.div.style.bottom)) ) this.div.style.bottom = '0px';
  870.   this.onDragStart = function(){};
  871.   this.onDragEnd = function(){};
  872.   this.onDrag = function(){};
  873.   this.onClick = function(){};
  874.   //this.mouseDown = addEventHandler( this.handle, 'mousedown', this.start, this );
  875. };
  876.  
  877. Drag.prototype.start = function( e )
  878. {
  879.   // this.mouseUp = addEventHandler( this.handle, 'mouseup', this.end, this );
  880.   e = Drag.fixE( e );
  881.   this.started = new Date();
  882.   var y = this.startY = parseInt(this.div.style.bottom);
  883.   var x = this.startX = parseInt(this.div.style.right);
  884.   this.onDragStart( x, y );
  885.   this.lastMouseX = e.clientX;
  886.   this.lastMouseY = e.clientY;
  887.   //this.documentMove = addEventHandler( document, 'mousemove', this.drag, this );
  888.   this.documentStop = addEventHandler( document, 'mouseup', this.end, this );
  889.   if( e.preventDefault ) e.preventDefault();
  890.   return false;
  891. };
  892.  
  893. Drag.prototype.drag = function( e )
  894. {
  895.   e = Drag.fixE( e );
  896.   var ey = e.clientY;
  897.   var ex = e.clientX;
  898.   var y = parseInt(this.div.style.bottom);
  899.   var x = parseInt(this.div.style.right );
  900.   var nx = x - ex + this.lastMouseX;
  901.   var ny = y - ey + this.lastMouseY;
  902.   this.div.style.right    = nx + 'px';
  903.   this.div.style.bottom    = ny + 'px';
  904.   this.lastMouseX    = ex;
  905.   this.lastMouseY    = ey;
  906.   this.onDrag( nx, ny );
  907.   if( e.preventDefault ) e.preventDefault();
  908.   return false;
  909. };
  910.  
  911. Drag.prototype.end = function()
  912. {
  913.   //removeEventHandler( document, 'mousemove', this.documentMove );
  914.   removeEventHandler( document, 'mouseup', this.documentStop );
  915.   var time = (new Date()) - this.started;
  916.   var x = parseInt(this.div.style.right),  dx = this.startX - x;
  917.   var y = parseInt(this.div.style.bottom), dy = this.startY - y;
  918.   this.onDragEnd( x, y, dx, dy, time );
  919.   if( (dx*dx + dy*dy) < (4*4) && time < 1e3 )
  920.     this.onClick( x, y, dx, dy, time );
  921. };
  922.  
  923. function removeEventHandler( target, eventName, eventHandler )
  924. {
  925.   if( target.addEventListener )
  926.     target.removeEventListener( eventName, eventHandler, true );
  927.   else if( target.attachEvent )
  928.     target.detachEvent( 'on' + eventName, eventHandler );
  929. }
  930.  
  931. function addEventHandler( target, eventName, eventHandler, scope )
  932. {
  933.   var f = scope ? function(){ eventHandler.apply( scope, arguments ); } : eventHandler;
  934.   if( target.addEventListener )
  935.     target.addEventListener( eventName, f, true );
  936.   else if( target.attachEvent )
  937.     target.attachEvent( 'on' + eventName, f );
  938.   return f;
  939. }
  940.  
  941. function burro( location, isbn )
  942. {
  943.   if( debug > 1 ) PRO_log( 'adding burro' );
  944.   var handle = document.createElement( 'div' );
  945.   var root = document.createElement( 'div' );
  946.   var box = document.createElement( 'div' ), i, h;
  947.   with( root.style )
  948.   {
  949.     position = 'absolute';
  950.     top = right = '15px';
  951.   }
  952.   handle.style.padding = '4px';
  953.   handle.title = 'Click title to expand, collapse or drag';
  954.   with( box.style )
  955.   {
  956.     position = 'relative';
  957.     zIndex = '1000';
  958.  
  959.     backgroundColor = '#FFC';
  960.     border = '1px solid orange';
  961.     padding = '0px';
  962.     textAlign = 'left';
  963.     font = '8pt sans-serif';
  964.     width = '240px';
  965.     marginBottom = '15px';
  966.  
  967.     opacity = '0.93';
  968.     filter = 'alpha(opacity=90)';
  969.   }
  970.  
  971.   var carrot = document.createElement( 'img' );
  972.   carrot.style.top = '-10px';
  973.   carrot.src = Icons.carrotRight;
  974.   carrot.id = 'hide_show_carrot';
  975.   handle.appendChild( carrot );
  976.  
  977.   var title_image = document.createElement( 'img' );
  978.   title_image.style.marginLeft = '6px';
  979.   title_image.src = Icons.title;
  980.   handle.appendChild( title_image );
  981.  
  982.   var close = document.createElement( 'img' );
  983.   close.src = Icons.close;
  984.   with( close.style )
  985.   {
  986.     position = 'absolute';
  987.     right = '3px';
  988.     top = '3px';
  989.     margin = '2px';
  990.     width = '12px';
  991.     height = '12px';
  992.     backgroundColor = '#FFB';
  993.     border = 'none';
  994.     lineHeight = '8px';
  995.     textAlign = 'center';
  996.   }
  997.   close.setAttribute( 'title', 'Click to remove' );
  998.   addEventHandler( close, 'click', function(){ document.body.removeChild( root ); } );
  999.   handle.appendChild( close );
  1000.  
  1001.   var about = document.createElement( 'a' );
  1002.   var about_img = document.createElement( 'img' );
  1003.   about_img.src = Icons.about;
  1004.   about_img.style.border = 'none';
  1005.   about.appendChild( about_img );
  1006.   with( about.style )
  1007.   {
  1008.     position = 'absolute';
  1009.     right = '18px';
  1010.     top = '3px';
  1011.     margin = '2px';
  1012.     width = '12px';
  1013.     height = '12px';
  1014.     backgroundColor = '#FFB';
  1015.     lineHeight = '12px';
  1016.     textAlign = 'center';
  1017.     textDecoration = 'none';
  1018.   }
  1019.   about.title = 'OverStimulate / modified by Reify and Johan Sundstr├╢m';
  1020.   about.href = 'http://overstimulate.com/articles/2005/04/24/greasemonkey-book-burro-find-cheap-books';
  1021.   handle.appendChild( about );
  1022.   box.appendChild( handle );
  1023.  
  1024.   var table = document.createElement( 'div' );
  1025.   with( table.style )
  1026.   {
  1027.     marginTop = '1px';
  1028.     marginBottom = '3px';
  1029.     padding = '0';
  1030.     width = '100%';
  1031.     font = '10pt sans-serif';
  1032.     display = 'none';
  1033.   }
  1034.   table.id = 'bookburro-pricesTable';
  1035.   for( i=0; i<handlers.length; i++ )
  1036.   {
  1037.     h = handlers[i];
  1038.     table.appendChild( addSite( h.bookURL.replace( /%s/g, isbn ), h.name, h.id ) );
  1039.   }
  1040.   box.appendChild( table );
  1041.  
  1042.   var zip = function()
  1043.   {
  1044.     box.opened = !box.opened;
  1045.     var pricesTable = document.getElementById( 'bookburro-pricesTable' );
  1046.     var carrot = document.getElementById( 'hide_show_carrot' );
  1047.     if( box.opened ) // pricesTable.style.display == 'none'
  1048.     {
  1049.       if( !hasFetched ) runQueries( isbn );
  1050.       hasFetched = true;
  1051.       pricesTable.style.display = document.all ? 'block' : 'table';
  1052.       carrot.src = Icons.carrotDown;
  1053.     }
  1054.     else
  1055.     {
  1056.       pricesTable.style.display = 'none';
  1057.       carrot.src = Icons.carrotRight;
  1058.     }
  1059.   };
  1060.   //addEventHandler( box, 'click', zip );
  1061.   root.appendChild( box );
  1062.   document.body.appendChild( root );
  1063.   //handle.drag = new Drag( handle, box );
  1064.   //handle.drag.onClick = zip;
  1065.   zip();
  1066.   if( debug > 1 ) PRO_log( 'added burro' );
  1067. }
  1068.  
  1069. function init()
  1070. {
  1071.   var i, h, getISBN, isbn;
  1072.   for( i=0; i<handlers.length; i++ )
  1073.   {
  1074.     h = handlers[i];
  1075.     if( h.hostname && location.hostname.match( h.hostname ) )
  1076.       try {
  1077.     if( debug ) PRO_log( h.name +' matched at '+ location.hostname );
  1078.     getISBN = h.getISBN || /isbn[\/=]([0-9X]{10})(&|\?|\.|$)/i || /ISBN[\/=]([0-9X]{10})(&|\?|\.|$)/i;
  1079.     switch( typeof getISBN )
  1080.     {
  1081.       case 'string':
  1082.         getISBN = new RegExp( getISBN );
  1083.         // fall-through
  1084.       case 'function':
  1085.       case 'object':
  1086.         if( getISBN.exec ) // a regexp
  1087.         {
  1088.           var rx = getISBN;
  1089.           getISBN = function(){ return location.href.match( rx )[1]; };
  1090.         }
  1091.         isbn = getISBN();
  1092.         break;
  1093.  
  1094.       default:
  1095.         alert( 'getISBN handler for '+ h.name +' (id '+ h.id +
  1096.            ') is of illegal type '+ (typeof getISBN) +'! Ignoring.' );
  1097.         continue;
  1098.     }
  1099.     if( (isbn = checkISBN( isbn )) ){
  1100.       burro( h.id, isbn );
  1101.         }
  1102.     else if( debug )
  1103.       PRO_log( 'Failed to find ISBN for '+ h.name +' (id '+ h.id +')' );
  1104.       } catch (e) { if( debug ) PRO_log( e ); }
  1105.   }
  1106. }
  1107.  
  1108. // addEventHandler(document, 'load', init);
  1109. init();
  1110.